import { parseName as parseNameNs } from '../utils'

function pick(target, properties) {
  var result = {}

  var obj = Object(target)

  properties.forEach(prop => {
    if (prop in obj) {
      result[prop] = target[prop]
    }
  })

  return result
}

/**
 * A utility to build element descriptors.
 */
export default function DescriptorBuilder(nameNs) {
  this.ns = nameNs
  this.name = nameNs.name
  this.allTypes = []
  this.allTypesByName = {}
  this.properties = []
  this.propertiesByName = {}
}

DescriptorBuilder.prototype.build = function() {
  return pick(this, [
    'ns',
    'name',
    'allTypes',
    'allTypesByName',
    'properties',
    'propertiesByName',
    'bodyProperty',
    'idProperty'
  ])
}

/**
 * Add property at given index.
 *
 * @param {Object} p
 * @param {Number} [idx]
 * @param {Boolean} [validate=true]
 */
DescriptorBuilder.prototype.addProperty = function(p, idx, validate) {
  if (typeof idx === 'boolean') {
    validate = idx
    idx = undefined
  }

  this.addNamedProperty(p, validate !== false)

  var properties = this.properties

  if (idx !== undefined) {
    properties.splice(idx, 0, p)
  } else {
    properties.push(p)
  }
}

DescriptorBuilder.prototype.replaceProperty = function(
  oldProperty,
  newProperty,
  replace
) {
  var oldNameNs = oldProperty.ns

  var props = this.properties

  var propertiesByName = this.propertiesByName

  var rename = oldProperty.name !== newProperty.name

  if (oldProperty.isId) {
    if (!newProperty.isId) {
      throw new Error(
        'property <' +
          newProperty.ns.name +
          '> must be id property ' +
          'to refine <' +
          oldProperty.ns.name +
          '>'
      )
    }

    this.setIdProperty(newProperty, false)
  }

  if (oldProperty.isBody) {
    if (!newProperty.isBody) {
      throw new Error(
        'property <' +
          newProperty.ns.name +
          '> must be body property ' +
          'to refine <' +
          oldProperty.ns.name +
          '>'
      )
    }

    // TODO: Check compatibility
    this.setBodyProperty(newProperty, false)
  }

  // validate existence and get location of old property
  var idx = props.indexOf(oldProperty)
  if (idx === -1) {
    throw new Error(
      'property <' + oldNameNs.name + '> not found in property list'
    )
  }

  // remove old property
  props.splice(idx, 1)

  // replacing the named property is intentional
  //
  //  * validate only if this is a "rename" operation
  //  * add at specific index unless we "replace"
  //
  this.addProperty(newProperty, replace ? undefined : idx, rename)

  // make new property available under old name
  propertiesByName[oldNameNs.name] = propertiesByName[
    oldNameNs.localName
  ] = newProperty
}

DescriptorBuilder.prototype.redefineProperty = function(
  p,
  targetPropertyName,
  replace
) {
  var nsPrefix = p.ns.prefix
  var parts = targetPropertyName.split('#')

  var name = parseNameNs(parts[0], nsPrefix)
  var attrName = parseNameNs(parts[1], name.prefix).name

  var redefinedProperty = this.propertiesByName[attrName]
  if (!redefinedProperty) {
    throw new Error('refined property <' + attrName + '> not found')
  } else {
    this.replaceProperty(redefinedProperty, p, replace)
  }

  delete p.redefines
}

DescriptorBuilder.prototype.addNamedProperty = function(p, validate) {
  var ns = p.ns

  var propsByName = this.propertiesByName

  if (validate) {
    this.assertNotDefined(p, ns.name)
    this.assertNotDefined(p, ns.localName)
  }

  propsByName[ns.name] = propsByName[ns.localName] = p
}

DescriptorBuilder.prototype.removeNamedProperty = function(p) {
  var ns = p.ns

  var propsByName = this.propertiesByName

  delete propsByName[ns.name]
  delete propsByName[ns.localName]
}

DescriptorBuilder.prototype.setBodyProperty = function(p, validate) {
  if (validate && this.bodyProperty) {
    throw new Error(
      'body property defined multiple times ' +
        '(<' +
        this.bodyProperty.ns.name +
        '>, <' +
        p.ns.name +
        '>)'
    )
  }

  this.bodyProperty = p
}

DescriptorBuilder.prototype.setIdProperty = function(p, validate) {
  if (validate && this.idProperty) {
    throw new Error(
      'id property defined multiple times ' +
        '(<' +
        this.idProperty.ns.name +
        '>, <' +
        p.ns.name +
        '>)'
    )
  }

  this.idProperty = p
}

DescriptorBuilder.prototype.assertNotDefined = function(p, name) {
  var propertyName = p.name

  var definedProperty = this.propertiesByName[propertyName]

  if (definedProperty) {
    throw new Error(
      'property <' +
        propertyName +
        '> already defined; ' +
        'override of <' +
        definedProperty.definedBy.ns.name +
        '#' +
        definedProperty.ns.name +
        '> by ' +
        '<' +
        p.definedBy.ns.name +
        '#' +
        p.ns.name +
        '> not allowed without redefines'
    )
  }
}

DescriptorBuilder.prototype.hasProperty = function(name) {
  return this.propertiesByName[name]
}

DescriptorBuilder.prototype.addTrait = function(t, inherited) {
  var typesByName = this.allTypesByName

  var types = this.allTypes

  var typeName = t.name

  if (typeName in typesByName) {
    return
  }

  if (t.properties) {
    t.properties.forEach(p => {
      // clone property to allow extensions
      p = Object.assign({}, p, {
        name: p.ns.localName,
        inherited: inherited
      })

      Object.defineProperty(p, 'definedBy', {
        value: t
      })

      var replaces = p.replaces

      var redefines = p.redefines

      // add replace/redefine support
      if (replaces || redefines) {
        this.redefineProperty(p, replaces || redefines, replaces)
      } else {
        if (p.isBody) {
          this.setBodyProperty(p)
        }
        if (p.isId) {
          this.setIdProperty(p)
        }
        this.addProperty(p)
      }
    })
  }

  types.push(t)
  typesByName[typeName] = t
}
